MEV Attacks¶

Imports¶

In [29]:
import pandas as pd
import plotly.io as pio
from sqlalchemy import create_engine

from mev_attacks import utils
import requests

pd.options.plotting.backend = "plotly"
pio.renderers.default="notebook"
pio.templates.default = "seaborn"
In [30]:
DB_AVAIL = False
if DB_AVAIL:
    user = "postgres"
    password="password"
    host = "localhost"
    engine   = create_engine(f'postgresql+psycopg2://{user}:{password}@{host}/mev_inspect', pool_recycle=3600)
    conn = engine.connect();

Sandwich Attack¶

Description¶

A sandwich attack involves executing transactions immediately before and immediately after a swap transaction on a decentralized exchange that sets prices according to a known formula. Many of the largest decentralized exchanges utilize a constant product formula such that the product of the supply of each token in a pool must remain constant.

For example, if there were a pool of a pair of tokens A and B each with an initial supply of 10, the product would need to remain constant at 100. To determine the number of B tokens that would be received for 1 A token, you would take 100 divided by 11, the new supply of A, to determine that after the transaction there needs to be 9.090909 B tokens in order to keep the product of the supplies constant at 100. Therefore, you could swap 1 A token for 0.90909 B tokens.

A B k / price
$t_0$ 10.0000 10.0000 100.0000
$user$ 1.0000 -0.9091 0.9091
$t_3$ 11.0000 9.0909 100.0000

Since transactions are written together in blocks, the final amount received for an individual transaction depends on the other transactions in the block that get executed before it. In order to protect users from unlimited exposure to changes in their received price, one of the parameters they specify is the amount of slippage they will suffer before their transaction will simply not be executed.

The sandwich attack will execute transactions on either side of a swap transaction to capture the maximum allowable slippage. The default slippage for Uniswap is 0.5%, but it can be set to anything. To illustrate the nature of the attack, assume that the allowable slippage in the above transaction was specified to be 10%, meaning that as few as 0.8181 B tokens could be received for 1 A token and the transaction would still be executed.

To execute an attack to capture the maximum possible slippage of 0.0909 a searcher would first determine what transaction they need to execute such that the supply of A and B tokens in the pool when the user’s transaction is executed results in the user receiving 0.8181 B tokens. Then after the user’s transaction, the searcher executes another transaction in the opposite direction that leaves the supply in the pools where it would have been without the attack, netting them the slippage at the user’s expense.

$$ \begin{align*} Let A_1 &= \text{the supply of token A after the attacker's initial transaction} \\ Let B_1 &= \text{the supply of token B after the attacker's initial transaction} \\ \end{align*} $$

and in order for $A * B = 100$ to be true,

$$ \begin{align*} (A_1 + 1.0000) * (B_1 - 0.8181) &= 100 \\ (A_1 + 1.0000) * (\frac{100}{A_1} - 0.8181) &= 100 \\ A_1 = 10.5667 \end{align*} $$

which means the swap the searcher executes initially is for 0.5363 B tokens in exchange for 0.5667 A tokens. After the user's transaction the searcher swaps their 0.5667 A tokens for 0.4454 B tokens to net 0.0909 B tokens from the sandwich attack.

A B k / price
$t_0$ 10.0000 10.0000 100.0000
$pre$ 0.5667 -0.5363 0.9463
$t_1$ 10.5667 9.4637 100.0000
$user$ 1.0000 -0.8182 -0.8182
$t_2$ 11.5667 8.6455 100.0000
$post$ -0.5667 0.4454 0.7860
$t_3$ 11.0000 9.0909 100.0000
$profit$ 0.0909

Examples¶

In [31]:
if False:
    df_sandwich = pd.read_sql("SELECT * from sandwiches", conn)
    df_sandwich.to_csv("data/sandwiches.csv", index=False)
df_sandwich = pd.read_csv("data/sandwiches.csv")
In [32]:
df_sandwich.head()
Out[32]:
id created_at block_number sandwicher_address frontrun_swap_transaction_hash frontrun_swap_trace_address backrun_swap_transaction_hash backrun_swap_trace_address profit_token_address profit_amount
0 783a005e-b3fa-401c-9a38-bfe13e701346 2022-01-13 21:34:44.042851 12634570.0 0x00000000003b3cc22af3ae1eac0440bcee416b40 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... [1] 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... [1] NaN NaN
1 4b83d760-b597-457a-84a6-ca017a8ea444 2022-01-13 21:34:44.042851 12634575.0 0x000000000035b5e5ad9019092c665357240f594e 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... [1] 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... [1] NaN NaN
2 9929faff-c29a-41ff-a08d-3e8868f1bcb3 2022-01-13 21:34:44.042851 12634579.0 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... [1] 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... [1] NaN NaN
3 28ade08a-f5f3-4554-91ad-d4a550bc14c7 2022-01-13 21:34:44.417610 12634558.0 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... [1] 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... [1] NaN NaN
4 fccf0e9a-8745-43fa-801f-e3551e824ff0 2022-01-13 21:34:44.417610 12634558.0 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... [2] 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... [4] NaN NaN
In [33]:
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].frontrun_swap_transaction_hash}")
print(f"https://etherscan.io/tx/{df_sandwich.iloc[0].backrun_swap_transaction_hash}")
https://etherscan.io/tx/0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec42ddf7fc4f880bc3def16
https://etherscan.io/tx/0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6b2d34e39e0d1bf7c64b3

The front run transaction was to swap 87.0484 WETH for 416,650.9839 KEEP and the back run was to swap the 416,650.9839 KEEP back for 88.3490 WETH, for a gross profit of 1.2265 WETH.

Check Profit Calculation¶

Here we check the simple profit sandwich profit calculation for a sequence of 65,000 blocks from 2021-06-14 to 2021-06-24.

In [34]:
df_sandwich
Out[34]:
id created_at block_number sandwicher_address frontrun_swap_transaction_hash frontrun_swap_trace_address backrun_swap_transaction_hash backrun_swap_trace_address profit_token_address profit_amount
0 783a005e-b3fa-401c-9a38-bfe13e701346 2022-01-13 21:34:44.042851 12634570.0 0x00000000003b3cc22af3ae1eac0440bcee416b40 0xa7b6b85b4e3055a27b0bb64b611eb42ca24b29d42ec4... [1] 0xa9c8dec6ca95e7df25102b708b5c97dad3b8c717e6a6... [1] NaN NaN
1 4b83d760-b597-457a-84a6-ca017a8ea444 2022-01-13 21:34:44.042851 12634575.0 0x000000000035b5e5ad9019092c665357240f594e 0xac8fe2ca3f4b54e52c2c49bdd01dca4f2a04479662e0... [1] 0xc8c47e551377a41594c3927d39049cd899e560a0c6b5... [1] NaN NaN
2 9929faff-c29a-41ff-a08d-3e8868f1bcb3 2022-01-13 21:34:44.042851 12634579.0 0x83f893cc6610bfc695f8e2d4cd0e6d3033dec77e 0x4317ab2ba7b140e51689b3670278c52aa2520ab18d7d... [1] 0x9677755d9072e5c35ca8f4832ff013252ba34756ce58... [1] NaN NaN
3 28ade08a-f5f3-4554-91ad-d4a550bc14c7 2022-01-13 21:34:44.417610 12634558.0 0x00000000b7ca7e12dcc72290d1fe47b2ef14c607 0xe71f571da7cd57d5c27b4124451357ccb51955346fd0... [1] 0x0c01e67324cf615fedb85451e1c13149714c49b0a515... [1] NaN NaN
4 fccf0e9a-8745-43fa-801f-e3551e824ff0 2022-01-13 21:34:44.417610 12634558.0 0x1d6e8bac6ea3730825bde4b005ed7b2b39a2932d 0x919f5806a5104db1d91f4bd54c44bd9553c443e58700... [2] 0x3db6116c5423c88ff6e29065f55bef0be241a0c27e0d... [4] NaN NaN
... ... ... ... ... ... ... ... ... ... ...
41834 ef17d513-7eb3-4340-848f-23cce359a979 2022-01-18 00:21:31.684638 14025471.0 0x7cf09d7a9a74f746edcb06949b9d64bcd9d1604f 0x65e94424bb54a540de66feb6aad26924b29c779ec603... [1] 0x6f2bf1772fe1dc9260a13ca30e09d353e5193df209f3... [1] NaN 1.082515e+17
41835 3e90b067-a89b-4dfd-9d04-772fa3b33b0f 2022-01-18 00:21:31.684638 14025475.0 0x499dd900f800fd0a2ed300006000a57f00fa009b 0x7d4fda37099ac9a7692db3641a0dd117f3d58ae2b67e... [0] 0xb4f24da6198fe3bbd1ad468e7daa85e33bafa6de8e40... [3] NaN 7.293653e+16
41836 8ef44968-5e19-46a9-abaf-559c241e2d5c 2022-01-18 00:21:52.525554 14025481.0 0x000000005736775feb0c8568e7dee77222a26880 0x8e7055b6297d480c2541ad57d2e5d5d4080c2ecf8887... [1] 0x2c2a514a387a72f92a76f59755402f7e667c296205f8... [1] NaN 1.436136e+17
41837 335fb14e-cabf-48b6-a4e9-87d69210d3ce 2022-01-18 00:21:52.525554 14025482.0 0x00000000500e2fece27a7600435d0c48d64e0c00 0xbb63d3f7b69c1c231c6d2028309365f0aff4dcd4a18a... [1] 0xe817af00f3fd460ce2ac3ebb3f9307ec3a781fa89578... [1] NaN 7.034793e+17
41838 e02ffbd9-fcea-454f-8901-0d58ef05ddfc 2022-01-18 00:24:20.741957 14025662.0 0x00000000500e2fece27a7600435d0c48d64e0c00 0x5fbf4018974e41c0a2ea0e7c2e842bb07b80b6724f7b... [1] 0xece083062760678b37910671f5cd65208d52f60d9758... [1] NaN 5.807259e+16

41839 rows × 10 columns

There are duplicates in the swaps table. This is a pull of 10,000 and ~4,500 have at least one duplicate.

In [35]:
if False:
    df_swap = pd.read_sql("SELECT * FROM swaps LIMIT 10000", conn)
    df_swap.to_csv("data/swaps.csv", index=False)
df_swap = pd.read_csv("data/swaps.csv")
df_swap.columns
Out[35]:
Index(['created_at', 'abi_name', 'transaction_hash', 'block_number',
       'protocol', 'contract_address', 'from_address', 'to_address',
       'token_in_address', 'token_in_amount', 'token_out_address',
       'token_out_amount', 'trace_address', 'error', 'transaction_position'],
      dtype='object')
In [36]:
df_swap.transaction_hash.value_counts().hist()
In [37]:
q = """
    SELECT 
        id,
        frontrun_swap_transaction_hash,
        backrun_swap_transaction_hash,
        swap_front.transaction_hash front_transaction_hash,
        swap_front.token_in_amount front_token_in_amount,
        swap_front.token_out_amount front_token_out_amount,
        swap_back.token_in_amount back_token_in_amount,
        swap_back.token_out_amount back_token_out_amount
    FROM sandwiches
    LEFT JOIN swaps swap_front ON sandwiches.frontrun_swap_transaction_hash = swap_front.transaction_hash
    LEFT JOIN swaps swap_back ON sandwiches.backrun_swap_transaction_hash = swap_back.transaction_hash
"""
In [38]:
if False:
    df_profit = pd.read_sql(q, conn)
    df_profit.to_csv("data/profit.csv", index=False)
else:
    df_profit = pd.read_csv("data/profit.csv")

df_profit.id.value_counts().hist()
In [39]:
df_profit["profit_front_token_in"] = df_profit.back_token_out_amount - df_profit.front_token_in_amount
df_profit["profit_front_token_out"] = df_profit.back_token_in_amount - df_profit.front_token_out_amount

Filtering out duplicates as first pass to check profit calcs - need to figure out why dupes exist and how to properly filter out.

In [40]:
df_profit = df_profit.drop_duplicates("id")
df_profit["profit_bin_front_out"] = pd.qcut(df_profit.profit_front_token_out / 1e18, 50, duplicates="drop")
df_profit["profit_bin_front_in"] = pd.qcut(df_profit.profit_front_token_in / 1e18, 50, duplicates="drop")

In theory, the front token out should exactly equal the back token in, with the actual profit being the delta between the front token in and the back token out. Here we check to see how often the front token out profit deviates from zero.

In [41]:
df_profit_freq = df_profit.groupby("profit_bin_front_out").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token Out Profit Freq")

Just to check to see whether profit might be being taken on the other side, we check the profit on the front token in.

In [42]:
df_profit_freq = df_profit.groupby("profit_bin_front_in").count()["id"]
df_profit_freq.index = df_profit_freq.index.astype(str)
df_profit_freq.plot(kind="bar", title="Front Token In Profit Freq")

After Adding in New Profit Amount Column¶

In [43]:
if DB_AVAIL:
    df_sand_new = pd.read_sql("SELECT * FROM sandwiches WHERE block_number > 14026126", conn)
    df_sand_new.to_csv("data/sandwiches_new.csv", index=False)
else:
    df_sand_new = pd.read_csv("data/sandwiches_new.csv")
df_sand_new.columns
Out[43]:
Index(['id', 'created_at', 'block_number', 'sandwicher_address',
       'frontrun_swap_transaction_hash', 'frontrun_swap_trace_address',
       'backrun_swap_transaction_hash', 'backrun_swap_trace_address',
       'profit_token_address', 'profit_amount'],
      dtype='object')
In [44]:
df_sand_new
Out[44]:
id created_at block_number sandwicher_address frontrun_swap_transaction_hash frontrun_swap_trace_address backrun_swap_transaction_hash backrun_swap_trace_address profit_token_address profit_amount
0 81c6efeb-c064-4b1f-ae93-f4eaa42b9cfb 2022-01-18 01:06:43.957480 14026161.0 0x7cf09d7a9a74f746edcb06949b9d64bcd9d1604f 0x2ce8417bc91a24482f2bea344e7436aa62ed7391ddf5... [0] 0xb27b5715fbde07b034a9042a530a31e9ac013a185c2a... [0] 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 3.488567e+17
1 9927470f-b185-4809-af1d-f1bf5dbd49dd 2022-01-18 01:06:43.957480 14026163.0 0x000000000035b5e5ad9019092c665357240f594e 0xe794c35ec49bbb823f891b6410b13c67c71b8a067f15... [1] 0x0a8595b5ba0998101dd0ce63bde19898ce31bc454b79... [1] 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 2.607730e+16
2 5c6e0b74-6fcc-4dbd-a3cd-97925b72855b 2022-01-18 01:06:43.957480 14026165.0 0x000000000035b5e5ad9019092c665357240f594e 0x12a5aecd9f6f770d0c46ee87e2e14757c9c43c598ce2... [1] 0xaefd22c5b1384704b8c5cc90ea3f0a68952d984c05f3... [1] 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 3.568630e+16
3 e8724f12-1e78-4f63-9450-e1e936ab1cd6 2022-01-18 01:12:02.777742 14026225.0 0x01ff6318440f7d5553a82294d78262d5f5084eff 0xf331484d2de0a2439db8f7f7e5b8b61a6d65b49180ce... [0] 0x1f3e95df8f7db63887a0371974b932eeeebb2b5dcbc1... [0] 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 1.635118e+17
In [45]:
df_sand_new.sort_values("profit_amount", ascending=False).plot(kind="bar", x="id", y="profit_amount", title="Profit Amount")
In [46]:
df_sand_new[df_sand_new.profit_amount < 10 * 1e18].groupby("profit_token_address").sum()["profit_amount"].plot(kind="bar", title="Profit Amount")